/*

 * Licensed to the Apache Software Foundation (ASF) under one

 * or more contributor license agreements.  See the NOTICE file

 * distributed with this work for additional information

 * regarding copyright ownership.  The ASF licenses this file

 * to you under the Apache License, Version 2.0 (the

 * "License"); you may not use this file except in compliance

 * with the License.  You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

package com.bff.gaia.unified.sdk.values;



import com.bff.gaia.unified.vendor.guava.com.google.common.collect.Lists;

import com.bff.gaia.unified.vendor.guava.com.google.common.reflect.Invokable;

import com.bff.gaia.unified.vendor.guava.com.google.common.reflect.Parameter;

import com.bff.gaia.unified.vendor.guava.com.google.common.reflect.TypeResolver;

import com.bff.gaia.unified.vendor.guava.com.google.common.reflect.TypeToken;



import javax.annotation.Nullable;

import java.io.Serializable;

import java.lang.reflect.*;

import java.util.List;



/**

 * A description of a Java type, including actual generic parameters where possible.

 *

 * <p>To prevent losing actual type arguments due to erasure, create an anonymous subclass with

 * concrete types:

 *

 * <pre>{@code

 * TypeDecriptor<List<String>> = new TypeDescriptor<List<String>>() {};

 * }</pre>

 *

 * <p>If the above were not an anonymous subclass, the type {@code List<String>} would be erased and

 * unavailable at run time.

 *

 * @param <T> the type represented by this {@link TypeDescriptor}

 */

public abstract class TypeDescriptor<T> implements Serializable {



  // This class is just a wrapper for TypeToken

  private final TypeToken<T> token;



  /**

   * Creates a {@link TypeDescriptor} wrapping the provided token. This constructor is private so

   * Guava types do not leak.

   */

  private TypeDescriptor(TypeToken<T> token) {

    this.token = token;

  }



  /**

   * Creates a {@link TypeDescriptor} representing the type parameter {@code T}. To use this

   * constructor properly, the type parameter must be a concrete type, for example {@code new

   * TypeDescriptor<List<String>>(){}}.

   */

  protected TypeDescriptor() {

    token = new TypeToken<T>(getClass()) {};

  }



  /**

   * Creates a {@link TypeDescriptor} representing the type parameter {@code T}, which should

   * resolve to a concrete type in the context of the class {@code clazz}.

   *

   * <p>Unlike {@link TypeDescriptor#TypeDescriptor(Class)} this will also use context's of the

   * enclosing instances while attempting to resolve the type. This means that the types of any

   * classes instantiated in the concrete instance should be resolvable.

   */

  protected TypeDescriptor(Object instance) {

    TypeToken<?> unresolvedToken = new TypeToken<T>(getClass()) {};



    // While we haven't fully resolved the parameters, refine it using the captured

    // enclosing instance of the object.

    unresolvedToken = TypeToken.of(instance.getClass()).resolveType(unresolvedToken.getType());



    if (hasUnresolvedParameters(unresolvedToken.getType())) {

      for (Field field : instance.getClass().getDeclaredFields()) {

        Object fieldInstance = getEnclosingInstance(field, instance);

        if (fieldInstance != null) {

          unresolvedToken =

              TypeToken.of(fieldInstance.getClass()).resolveType(unresolvedToken.getType());

          if (!hasUnresolvedParameters(unresolvedToken.getType())) {

            break;

          }

        }

      }

    }



    // Once we've either fully resolved the parameters or exhausted enclosing instances, we have

    // the best approximation to the token we can get.

    @SuppressWarnings("unchecked")

    TypeToken<T> typedToken = (TypeToken<T>) unresolvedToken;

    token = typedToken;

  }



  private boolean hasUnresolvedParameters(Type type) {

    if (type instanceof TypeVariable) {

      return true;

    } else if (type instanceof ParameterizedType) {

      ParameterizedType param = (ParameterizedType) type;

      for (Type arg : param.getActualTypeArguments()) {

        if (hasUnresolvedParameters(arg)) {

          return true;

        }

      }

    }

    return false;

  }



  /**

   * Returns the enclosing instance if the field is synthetic and it is able to access it, or

   * {@literal null} if not.

   */

  @Nullable

  private Object getEnclosingInstance(Field field, Object instance) {

    if (!field.isSynthetic()) {

      return null;

    }



    boolean accessible = field.isAccessible();

    try {

      field.setAccessible(true);

      return field.get(instance);

    } catch (IllegalArgumentException | IllegalAccessException e) {

      // If we fail to get the enclosing instance field, do nothing. In the worst case, we won't

      // refine the type based on information in this enclosing class -- that is consistent with

      // previous behavior and is still a correct answer that can be fixed by returning the correct

      // type descriptor.

      return null;

    } finally {

      field.setAccessible(accessible);

    }

  }



  /**

   * Creates a {@link TypeDescriptor} representing the type parameter {@code T}, which should

   * resolve to a concrete type in the context of the class {@code clazz}.

   */

  @SuppressWarnings("unchecked")

  protected TypeDescriptor(Class<?> clazz) {

    TypeToken<T> unresolvedToken = new TypeToken<T>(getClass()) {};

    token = (TypeToken<T>) TypeToken.of(clazz).resolveType(unresolvedToken.getType());

  }



  /** Returns a {@link TypeDescriptor} representing the given type. */

  public static <T> TypeDescriptor<T> of(Class<T> type) {

    return new SimpleTypeDescriptor<>(TypeToken.of(type));

  }



  /** Returns a {@link TypeDescriptor} representing the given type. */

  @SuppressWarnings("unchecked")

  public static TypeDescriptor<?> of(Type type) {

    return new SimpleTypeDescriptor<>((TypeToken<Object>) TypeToken.of(type));

  }



  /** Returns the {@link Type} represented by this {@link TypeDescriptor}. */

  public Type getType() {

    return token.getType();

  }



  /**

   * Returns the {@link Class} underlying the {@link Type} represented by this {@link

   * TypeDescriptor}.

   */

  public Class<? super T> getRawType() {

    return token.getRawType();

  }



  /** Returns the component type if this type is an array type, otherwise returns {@code null}. */

  public TypeDescriptor<?> getComponentType() {

    return new SimpleTypeDescriptor<>(token.getComponentType());

  }



  /** Returns the generic form of a supertype. */

  public final TypeDescriptor<? super T> getSupertype(Class<? super T> superclass) {

    return new SimpleTypeDescriptor<>(token.getSupertype(superclass));

  }



  /** Returns true if this type is known to be an array type. */

  public final boolean isArray() {

    return token.isArray();

  }



  /**

   * Returns a {@link TypeVariable} for the named type parameter. Throws {@link

   * IllegalArgumentException} if a type variable by the requested type parameter is not found.

   *

   * <p>For example, {@code new TypeDescriptor<List>(){}.getTypeParameter("T")} returns a {@code

   * TypeVariable<? super List>} representing the formal type parameter {@code T}.

   *

   * <p>Do not mistake the type parameters (formal type argument list) with the actual type

   * arguments. For example, if a class {@code Foo} extends {@code List<String>}, it does not make

   * sense to ask for a type parameter, because {@code Foo} does not have any.

   */

  public final TypeVariable<Class<? super T>> getTypeParameter(String paramName) {

    // Cannot convert TypeVariable<Class<? super T>>[] to TypeVariable<Class<? super T>>[]

    // due to how they are used here, so the result of getTypeParameters() cannot be used

    // without upcast.

    Class<?> rawType = getRawType();

    for (TypeVariable<?> param : rawType.getTypeParameters()) {

      if (param.getName().equals(paramName)) {

        @SuppressWarnings("unchecked")

        TypeVariable<Class<? super T>> typedParam = (TypeVariable<Class<? super T>>) param;

        return typedParam;

      }

    }

    throw new IllegalArgumentException(

        "No type parameter named " + paramName + " found on " + getRawType());

  }



  /** Returns true if this type is assignable from the given type. */

  public final boolean isSupertypeOf(TypeDescriptor<?> source) {

    return token.isSupertypeOf(source.token);

  }



  /** Return true if this type is a subtype of the given type. */

  public final boolean isSubtypeOf(TypeDescriptor<?> parent) {

    return token.isSubtypeOf(parent.token);

  }



  /** Returns a list of argument types for the given method, which must be a part of the class. */

  public List<TypeDescriptor<?>> getArgumentTypes(Method method) {

    Invokable<?, ?> typedMethod = token.method(method);



    List<TypeDescriptor<?>> argTypes = Lists.newArrayList();

    for (Parameter parameter : typedMethod.getParameters()) {

      argTypes.add(new SimpleTypeDescriptor<>(parameter.getType()));

    }

    return argTypes;

  }



  /**

   * Returns a {@link TypeDescriptor} representing the given type, with type variables resolved

   * according to the specialization in this type.

   *

   * <p>For example, consider the following class:

   *

   * <pre>{@code

   * class MyList implements List<String> { ... }

   * }</pre>

   *

   * <p>The {@link TypeDescriptor} returned by

   *

   * <pre>{@code

   * TypeDescriptor.of(MyList.class)

   *     .resolveType(Mylist.class.getMethod("get", int.class).getGenericReturnType)

   * }</pre>

   *

   * will represent the type {@code String}.

   */

  public TypeDescriptor<?> resolveType(Type type) {

    return new SimpleTypeDescriptor<>(token.resolveType(type));

  }



  /**

   * Returns a set of {@link TypeDescriptor TypeDescriptor}, one for each superclass as well as each

   * interface implemented by this class.

   */

  @SuppressWarnings("rawtypes")

  public Iterable<TypeDescriptor> getTypes() {

    List<TypeDescriptor> interfaces = Lists.newArrayList();

    for (TypeToken<?> interfaceToken : token.getTypes()) {

      interfaces.add(new SimpleTypeDescriptor<>(interfaceToken));

    }

    return interfaces;

  }



  /** Returns a set of {@link TypeDescriptor}s, one for each interface implemented by this class. */

  @SuppressWarnings("rawtypes")

  public Iterable<TypeDescriptor> getInterfaces() {

    List<TypeDescriptor> interfaces = Lists.newArrayList();

    for (TypeToken<?> interfaceToken : token.getTypes().interfaces()) {

      interfaces.add(new SimpleTypeDescriptor<>(interfaceToken));

    }

    return interfaces;

  }



  /** Returns a set of {@link TypeDescriptor}s, one for each superclass (including this class). */

  @SuppressWarnings("rawtypes")

  public Iterable<TypeDescriptor> getClasses() {

    List<TypeDescriptor> classes = Lists.newArrayList();

    for (TypeToken<?> classToken : token.getTypes().classes()) {

      classes.add(new SimpleTypeDescriptor<>(classToken));

    }

    return classes;

  }



  /**

   * Returns a new {@code TypeDescriptor} where the type variable represented by {@code

   * typeParameter} are substituted by {@code type}. For example, it can be used to construct {@code

   * Map<K, V>} for any {@code K} and {@code V} type:

   *

   * <pre>{@code

   * static <K, V> TypeDescriptor<Map<K, V>> mapOf(

   *     TypeDescriptor<K> keyType, TypeDescriptor<V> valueType) {

   *   return new TypeDescriptor<Map<K, V>>() {}

   *       .where(new TypeParameter<K>() {}, keyType)

   *       .where(new TypeParameter<V>() {}, valueType);

   * }

   * }</pre>

   *

   * @param <X> The parameter type

   * @param typeParameter the parameter type variable

   * @param typeDescriptor the actual type to substitute

   */

  @SuppressWarnings("unchecked")

  public <X> TypeDescriptor<T> where(

	  TypeParameter<X> typeParameter, TypeDescriptor<X> typeDescriptor) {

    return where(typeParameter.typeVariable, typeDescriptor.getType());

  }



  /**

   * A more general form of {@link #where(TypeParameter, TypeDescriptor)} that returns a new {@code

   * TypeDescriptor} by matching {@code formal} against {@code actual} to resolve type variables in

   * the current {@link TypeDescriptor}.

   */

  @SuppressWarnings("unchecked")

  public TypeDescriptor<T> where(Type formal, Type actual) {

    TypeResolver resolver = new TypeResolver().where(formal, actual);

    return (TypeDescriptor<T>) TypeDescriptor.of(resolver.resolveType(token.getType()));

  }



  /**

   * Returns whether this {@link TypeDescriptor} has any unresolved type parameters, as opposed to

   * being a concrete type.

   *

   * <p>For example:

   *

   * <pre>{@code

   * TypeDescriptor.of(new ArrayList<String>() {}.getClass()).hasUnresolvedTypeParameters()

   *   => false, because the anonymous class is instantiated with a concrete type

   *

   * class TestUtils {

   *   <T> ArrayList<T> createTypeErasedList() {

   *     return new ArrayList<T>() {};

   *   }

   * }

   *

   * TypeDescriptor.of(TestUtils.<String>createTypeErasedList().getClass())

   *   => true, because the type variable T got type-erased and the anonymous ArrayList class

   *   is instantiated with an unresolved type variable T.

   * }</pre>

   */

  public boolean hasUnresolvedParameters() {

    return hasUnresolvedParameters(getType());

  }



  @Override

  public String toString() {

    return token.toString();

  }



  /** Two type descriptor are equal if and only if they represent the same type. */

  @Override

  public boolean equals(Object other) {

    if (!(other instanceof TypeDescriptor)) {

      return false;

    } else {

      @SuppressWarnings("unchecked")

	  TypeDescriptor<?> descriptor = (TypeDescriptor<?>) other;

      return token.equals(descriptor.token);

    }

  }



  @Override

  public int hashCode() {

    return token.hashCode();

  }



  /**

   * A non-abstract {@link TypeDescriptor} for construction directly from an existing {@link

   * TypeToken}.

   */

  private static final class SimpleTypeDescriptor<T> extends TypeDescriptor<T> {

    SimpleTypeDescriptor(TypeToken<T> typeToken) {

      super(typeToken);

    }

  }

}